﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using GeoCode.Core;

namespace MSGeoCoder.Services.Yahoo
{
    /// <summary>
    /// 
    /// </summary>
    /// <remarks></remarks>
    public class YahooWhereGeoCoder : IGeoCoder
    {
        /// <summary>
        /// 
        /// </summary>
        public const string ServiceUrl = "http://where.yahooapis.com/v1";

        /// <summary>
        /// 
        /// </summary>
        public const string ServiceUrlNormal =
            "http://local.yahooapis.com/MapsService/V1/geocode?street={0}&city={1}&state={2}&zip={3}&appid={4}";

        /// <summary>
        /// The YAHOO application ID
        /// </summary>
        private readonly string _appId;

        /// <summary>
        /// 
        /// </summary>
        private XmlNamespaceManager _namespaceManager;

        /// <summary>
        /// Initializes a new instance of the <see cref="YahooWhereGeoCoder"/> class.
        /// </summary>
        /// <param name="appId">The app id.</param>
        /// <remarks></remarks>
        public YahooWhereGeoCoder(string appId)
        {
            if (String.IsNullOrEmpty(appId))
                throw new ArgumentNullException("appId");

            _appId = appId;
        }

        #region "Available Calls"

        /// <summary>
        /// Gets the place from lat LNG.
        /// </summary>
        /// <param name="lng">The LNG.</param>
        /// <param name="lat">The lat.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static YahooPlace GetPlaceFromLatLng(string lng, string lat)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format("http://www.geomojo.org/cgi-bin/reversegeocoder.cgi?long={0}&lat={1}", lng,
                                             lat));
            XNamespace gpx;
            gpx = "";
            XNamespace.Get("http://where.yahooapis.com/v1/schema.rng");

            IEnumerable<YahooPlace> waypoints = from waypoint in gpxDoc.Descendants(gpx + "place")
                                                select new YahooPlace
                                                           {
                                                               Weoid = 
                                                                   Convert.ToInt32(waypoint.Element(gpx + "woeid").Value),
                                                               Name = waypoint.Element(gpx + "name").Value,
                                                           };

            return waypoints.First();
        }
        
        //* /places: returns a collection of places.
        //* /place/{woeid}: returns a place resource.
        //* /place/{woeid}/parent: returns a place resource for the parent of a place.
        //* /place/{woeid}/ancestors: returns a collection of places in the parent hierarchy for a place.
        //* /place/{woeid}/belongtos: returns a collection of places that contain a place.
        //* /place/{woeid}/neighbors: returns a collection of places that are neighbors of a place.
        //* /place/{woeid}/siblings: returns a collection of places that are siblings of a place.
        //* /place/{woeid}/children: returns a collection of places that are children of a place.
        //* /placetypes: returns the complete collection of place types supported in GeoPlanet.
        //* /placetype: returns a place type resource.

        /// <summary>
        /// Returns a collection of places that match a specified place name, and optionally, a specified place type.
        /// The resources in the collection are long representations of each place (unless short representations are explicitly requested).
        /// </summary>
        /// <param name="PlaceName">Name of the place.</param>
        /// <param name="Focus">The focus.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public YahooPlace[] Places(string PlaceName, string Focus)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format(ServiceUrl + "/places.q('{0}')?appid={1}", HttpUtility.UrlEncode(PlaceName),
                                             _appId));

            IEnumerable<YahooPlace> waypoints = parseWaypoints(gpxDoc);

            return waypoints.ToArray();
        }

        /// <summary>
        /// Places the specified WOEID.
        /// </summary>
        /// <param name="WOEID">The WOEID.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public YahooPlace[] Place(string WOEID)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format(ServiceUrl + "/place/{0}?appid={1}", HttpUtility.UrlEncode(WOEID), _appId));

            IEnumerable<YahooPlace> waypoints = parseWaypoints(gpxDoc);

            return waypoints.ToArray();
        }


        /// <summary>
        /// Ancestorses the specified WOEID.
        /// </summary>
        /// <param name="WOEID">The WOEID.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public List<YahooPlace> Ancestors(string WOEID)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format(ServiceUrl + "/place/{0}/ancestors?appid={1}", HttpUtility.UrlEncode(WOEID),
                                             _appId));

            IEnumerable<YahooPlace> tmp = parseDoc(gpxDoc);
            return tmp.ToList();
        }

        /// <summary>
        /// Parses the doc.This is the basic parser for weoid,placeTypeName  and name   
        /// </summary>
        /// <param name="gpxDoc">The GPX doc.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private IEnumerable<YahooPlace> parseDoc(XDocument gpxDoc)
        {
            XNamespace gpx;
            gpx = XNamespace.Get("http://where.yahooapis.com/v1/schema.rng");

            IEnumerable<YahooPlace> waypoints = from waypoint in gpxDoc.Descendants(gpx + "place")
                                                select new YahooPlace
                                                           {
                                                               Weoid = 
                                                                   Convert.ToInt32(waypoint.Element(gpx + "woeid").Value),
                                                               PlaceTypeName =
                                                                   waypoint.Element(gpx + "placeTypeName").Value,
                                                               Name = waypoint.Element(gpx + "name").Value,
                                                           };

            return waypoints;
        }

        /// <summary>
        /// Parses the waypoints.
        /// </summary>
        /// <param name="gpxDoc">The GPX doc.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private IEnumerable<YahooPlace> parseWaypoints(XDocument gpxDoc)
        {
            XNamespace gpx;
            gpx = XNamespace.Get("http://where.yahooapis.com/v1/schema.rng");


            IEnumerable<YahooPlace> waypoints = from waypoint in gpxDoc.Descendants(gpx + "place")
                                                select new YahooPlace
                                                           {
                                                               Weoid =
                                                                   Convert.ToInt32(waypoint.Element(gpx + "woeid").Value),
                                                               PlaceTypeName =
                                                                   waypoint.Element(gpx + "placeTypeName") != null
                                                                       ? waypoint.Element(gpx + "placeTypeName").Value
                                                                       : null,
                                                               Name = waypoint.Element(gpx + "name") != null
                                                                          ? waypoint.Element(gpx + "name").Value
                                                                          : null,
                                                               Continent =
                                                                   waypoint.Element(gpx + "continent") != null
                                                                       ? waypoint.Element(gpx + "continent").Value
                                                                       : null,
                                                               Country =
                                                                   waypoint.Element(gpx + "country") != null
                                                                       ? waypoint.Element(gpx + "country").Value
                                                                       : null,
                                                               Admin1 =
                                                                   waypoint.Element(gpx + "admin1") != null
                                                                       ? waypoint.Element(gpx + "admin1").Value
                                                                       : null,
                                                               Admin2 =
                                                                  waypoint.Element(gpx + "admin2") != null
                                                                       ? waypoint.Element(gpx + "admin2").Value
                                                                       : null,
                                                               Admin3 =
                                                                   waypoint.Element(gpx + "admin3") != null
                                                                       ? waypoint.Element(gpx + "admin3").Value
                                                                       : null,
                                                               Locality1 =
                                                                   waypoint.Element(gpx + "locality1") != null
                                                                       ? waypoint.Element(gpx + "locality1").Value
                                                                       : null,
                                                               Locality2 =
                                                                   waypoint.Element(gpx + "locality2") != null
                                                                       ? waypoint.Element(gpx + "locality2").Value
                                                                       : null,
                                                               // Coordinates = new Location(Convert.ToDouble(waypoint.Element(gpx + "centroid").Element(gpx + "latitude")), Convert.ToDouble(waypoint.Element(gpx + "centroid").Element(gpx + "longitude")))
                                                               Coordinates =
                                                                   new Location(
                                                                   Convert.ToDouble(
                                                                       waypoint.Element(gpx + "centroid").Element(gpx +
                                                                                                                  "latitude")
                                                                           .Value),
                                                                   Convert.ToDouble(
                                                                       waypoint.Element(gpx + "centroid").Element(gpx +
                                                                                                                  "longitude")
                                                                           .Value))
                                                               // centroid = new LatLng {lat=
                                                               //postal = waypoint.Element(gpx + "postal") != null ? waypoint.Element(gpx + "postal").Value : null


                                                               //boundingBox = new BoundingBox{northeast = waypoint.Element(gpx+"boundingBox").Element(gpx+"")
                                                           };

            return waypoints;
        }

        /// <summary>
        /// Returns a collection of places that are neighbors of a place specifed by the WOEID.
        /// </summary>
        /// <param name="WOEID">The WOEID.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public List<YahooPlace> Neighbors(string WOEID)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format(ServiceUrl + "/place/{0}/neighbors?appid={1}", HttpUtility.UrlEncode(WOEID),
                                             _appId));

            IEnumerable<YahooPlace> tmp = parseDoc(gpxDoc);
            return tmp.ToList();
        }

        /// <summary>
        /// Returns a collection of places that are siblings of a place specifed by the WOEID.
        /// </summary>
        /// <param name="WOEID">The WOEID.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public List<YahooPlace> Siblings(string WOEID)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format(ServiceUrl + "/place/{0}/siblings?appid={1}", HttpUtility.UrlEncode(WOEID),
                                             _appId));

            IEnumerable<YahooPlace> tmp = parseDoc(gpxDoc);
            return tmp.ToList();
        }

        /// <summary>
        /// Returns a collection of places that are choldren of a place specifed by the WOEID.
        /// </summary>
        /// <param name="WOEID">The WOEID.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public List<YahooPlace> Children(string WOEID)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format(ServiceUrl + "/place/{0}/children?appid={1}", HttpUtility.UrlEncode(WOEID),
                                             _appId));

            IEnumerable<YahooPlace> tmp = parseDoc(gpxDoc);
            return tmp.ToList();
        }

        /// <summary>
        /// Returns a collection of places that contain a place specifed by the WOEID.
        /// </summary>
        /// <param name="WOEID">The WOEID.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public YahooPlace[] BelongsTo(string WOEID)
        {
            XDocument gpxDoc =
                XDocument.Load(String.Format(ServiceUrl + "/place/{0}/belongtos?appid={1}", HttpUtility.UrlEncode(WOEID),
                                             _appId));

            IEnumerable<YahooPlace> waypoints = parseDoc(gpxDoc); // parseWaypoints(gpxDoc);

            return waypoints.ToArray();
        }

        #endregion

        #region XML Parsing

        /// <summary>
        /// Creates the XML namespace manager.
        /// </summary>
        /// <param name="nav">The nav.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private XmlNamespaceManager CreateXmlNamespaceManager(XPathNavigator nav)
        {
            var nsManager = new XmlNamespaceManager(nav.NameTable);
            nsManager.AddNamespace("y", "urn:yahoo:maps");
            return nsManager;
        }

        /// <summary>
        /// Loads the XML response.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private XPathDocument LoadXmlResponse(WebResponse response)
        {
            using (Stream stream = response.GetResponseStream())
            {
                var doc = new XPathDocument(stream);
                return doc;
            }
        }

        /// <summary>
        /// Evaluates the X path.
        /// </summary>
        /// <param name="xpath">The xpath.</param>
        /// <param name="nav">The nav.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private string EvaluateXPath(string xpath, XPathNavigator nav)
        {
            XPathExpression exp = nav.Compile(xpath);
            exp.SetContext(_namespaceManager);
            return (string) nav.Evaluate(exp);
        }

        /// <summary>
        /// Retrieves the address.
        /// </summary>
        /// <param name="nav">The nav.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private Address RetrieveAddress(XPathNavigator nav)
        {
            AddressAccuracy accuracy = MapAccuracy(EvaluateXPath("string(@precision)", nav));

            double latitude = double.Parse(EvaluateXPath("string(y:Latitude)", nav), CultureInfo.InvariantCulture);
            double longitude = double.Parse(EvaluateXPath("string(y:Longitude)", nav), CultureInfo.InvariantCulture);

            string street = EvaluateXPath("string(y:Address)", nav);
            string city = EvaluateXPath("string(y:City)", nav);
            string state = EvaluateXPath("string(y:State)", nav);
            string postalCode = EvaluateXPath("string(y:Zip)", nav);
            string country = EvaluateXPath("string(y:Country)", nav);

            var addr = new Address
                           {Street = street, City = city, State = state, PostalCode = postalCode, Country = country};
            addr.ChangeLocation(new Location(latitude, longitude), accuracy);
            return addr;
        }

        /// <summary>
        /// Processes the web response.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private Address[] ProcessWebResponse(WebResponse response)
        {
            XPathDocument xmlDoc = LoadXmlResponse(response);
            XPathNavigator nav = xmlDoc.CreateNavigator();
            _namespaceManager = CreateXmlNamespaceManager(nav);

            XPathExpression exp = nav.Compile("y:places /y:place");
            exp.SetContext(_namespaceManager);
            XPathNodeIterator nodes = nav.Select(exp);

            var addresses = new List<Address>();
            while (nodes.MoveNext())
            {
                addresses.Add(RetrieveAddress(nodes.Current));
            }

            //XDocument gpxDoc = XDocument.Load("");
            //XNamespace gpx = GetGpxNameSpace();

            //var waypoints = from place in gpxDoc.Descendants("place")
            //                select new  
            //                {
            //                    City=  place.Attribute("lat").Value,
            //                    woeid = place.Element("woeid").Value,
            //                    placeTypeName =place.Element("placeTypeName ").Value,
            //                    Longitude = waypoint.Attribute("lon").Value,
            //                    Elevation = waypoint.Element(gpx + "ele") != null
            //                                    ?
            //                                        waypoint.Element(gpx + "ele").Value
            //                                    : null,
            //                    Name = waypoint.Element(gpx + "name") != null
            //                               ?
            //                                   waypoint.Element(gpx + "name").Value
            //                               : null,
            //                    Dt = waypoint.Element(gpx + "cmt") != null
            //                             ?
            //                                 waypoint.Element(gpx + "cmt").Value
            //                             : null
            //                };

            return addresses.ToArray();
        }

        #endregion

        /// <summary>
        /// Gets the app id.
        /// </summary>
        /// <remarks></remarks>
        public string AppId
        {
            get { return _appId; }
        }

        #region IGeoCoder Members

        /// <summary>
        /// Geoes the code.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public Address[] GeoCode(string address)
        {
            if (String.IsNullOrEmpty(address)) throw new ArgumentNullException("address");

            HttpWebRequest request = BuildWebRequest(address);
            return GeoCode(request);
        }

        /// <summary>
        /// Geoes the code.
        /// </summary>
        /// <param name="street">The street.</param>
        /// <param name="city">The city.</param>
        /// <param name="state">The state.</param>
        /// <param name="postalCode">The postal code.</param>
        /// <param name="country">The country.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public Address[] GeoCode(string street, string city, string state, string postalCode, string country)
        {
            //ignoring the country parameter since yahoo doesn't accept it
            HttpWebRequest request = BuildWebRequest(street, city, state, postalCode);
            return GeoCode(request);
        }

        #endregion

        /// <summary>
        /// Maps the accuracy.
        /// </summary>
        /// <param name="precision">The precision.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private AddressAccuracy MapAccuracy(string precision)
        {
            switch (precision)
            {
                case "address":
                    return AddressAccuracy.AddressLevel;
                case "street":
                    return AddressAccuracy.StreetLevel;
                case "zip+4":
                case "zip+2":
                case "zip":
                    return AddressAccuracy.PostalCodeLevel;
                case "city":
                    return AddressAccuracy.CityLevel;
                case "state":
                    return AddressAccuracy.StateLevel;
                case "country":
                    return AddressAccuracy.CountryLevel;
                default:
                    return AddressAccuracy.Unknown;
            }
        }

        /// <summary>
        /// Builds the web request.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private HttpWebRequest BuildWebRequest(string address)
        {
            string url = String.Format(ServiceUrl, HttpUtility.UrlEncode(address), _appId);
            var req = (HttpWebRequest) WebRequest.Create(url);
            req.Method = "GET";
            return req;
        }

        /// <summary>
        /// Builds the web request.
        /// </summary>
        /// <param name="street">The street.</param>
        /// <param name="city">The city.</param>
        /// <param name="state">The state.</param>
        /// <param name="postalCode">The postal code.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private HttpWebRequest BuildWebRequest(string street, string city, string state, string postalCode)
        {
            string url = String.Format(ServiceUrlNormal, HttpUtility.UrlEncode(street), HttpUtility.UrlEncode(city),
                                       HttpUtility.UrlEncode(state), HttpUtility.UrlEncode(postalCode), _appId);
            var req = (HttpWebRequest) WebRequest.Create(url);
            req.Method = "GET";
            return req;
        }

        /// <summary>
        /// Handles the web exception.
        /// </summary>
        /// <param name="ex">The ex.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private bool HandleWebException(WebException ex)
        {
            //yahoo returns a HTTP 400 Bad Request response when it gets an address it can't find
            if (((HttpWebResponse) ex.Response).StatusCode == HttpStatusCode.BadRequest)
                return true;
            return false;
        }

        /// <summary>
        /// Geoes the code.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private Address[] GeoCode(HttpWebRequest request)
        {
            try
            {
                using (WebResponse response = request.GetResponse())
                {
                    return ProcessWebResponse(response);
                }
            }
            catch (WebException ex)
            {
                if (!HandleWebException(ex))
                    throw;
                return new Address[0];
            }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>A <see cref="System.String"/> that represents this instance.</returns>
        /// <remarks></remarks>
        public override string ToString()
        {
            return String.Format("Yahoo GeoCoder: {0}", _appId);
        }
    }
}